本文同步更新於blog
情境:目前提供旅遊行程的方式
<?php
namespace App\BuilderPattern\Vacation;
class Program
{
    /**
     * @return array
     */
    public function getDomesticTravel()
    {
        //高速鐵路一日體驗
        return [
            'from' => 'Kaohsiung',
            'to' => 'Taipei',
            'day' => 1,
            'transport' => 'High Speed Rail'
        ];
    }
    /**
     * @return array
     */
    public function getInternationalTravel()
    {
        //東京五日遊
        return [
            'from' => 'Kaohsiung',
            'to' => 'Tokyo',
            'day' => 5,
            'transport' => 'Airplane',
            'hotel' => 'Disney Hotel'
        ];
    }
}
老闆希望我們能提供更簡便的方式,來規劃不同的旅遊行程。
讓我們用建造者模式改造它。
需求一:實作旅遊行程 (產品類別)
<?php
namespace App\BuilderPattern\Vacation;
class Itinerary
{
    /**
     * @var string
     */
    protected $from;
    /**
     * @var string
     */
    protected $to;
    /**
     * @var int
     */
    protected $day;
    /**
     * @var string
     */
    protected $hotel;
    /**
     * @var string
     */
    protected $transport;
    /**
     * @param string $name
     * @param string|int $value
     */
    public function __set($name, $value)
    {
        $this->$name = $value;
    }
    /**
     * @param string $name
     * @return string|int
     */
    public function __get($name)
    {
        return $this->$name;
    }
    /**
     * @return array
     */
    public function toArray()
    {
        $result = get_object_vars($this);
        foreach ($result as $name => $value) {
            if (is_null($value)) {
                unset($result[$name]);
            }
        }
        return $result;
    }
}
主要都是getter與setter方法。
當行程規劃好時,我們會透過 toArray() 方法來輸出。
需求二:實作行程建造者 (建造者類別)
<?php
namespace App\BuilderPattern\Vacation\Contracts;
use App\BuilderPattern\Vacation\Itinerary;
interface ItineraryPlanable
{
    public function from(string $from): self;
    public function to(string $to): self;
    public function spendDays(int $day): self;
    public function stayAt(string $hotel): self;
    public function travelBy(string $transport): self;
    public function getItinerary(): Itinerary;
}
<?php
namespace App\BuilderPattern\Vacation;
use App\BuilderPattern\Vacation\Itinerary;
use App\BuilderPattern\Vacation\Contracts\ItineraryPlanable;
class ItineraryBuilder implements ItineraryPlanable
{
    /**
     * @var Itinerary
     */
    protected $itinerary;
    public function __construct()
    {
        $this->itinerary = new Itinerary();
    }
    /**
     * @param string $from
     * @return self
     */
    public function from(string $from): self
    {
        $this->itinerary->from = $from;
        return $this;
    }
    /**
     * @param string $to
     * @return self
     */
    public function to(string $to): self
    {
        $this->itinerary->to = $to;
        return $this;
    }
    /**
     * @param integer $day
     * @return self
     */
    public function spendDays(int $day): self
    {
        $this->itinerary->day = $day;
        return $this;
    }
    /**
     * @param string $hotel
     * @return self
     */
    public function stayAt(string $hotel): self
    {
        $this->itinerary->hotel = $hotel;
        return $this;
    }
    /**
     * @param string $transport
     * @return self
     */
    public function travelBy(string $transport): self
    {
        $this->itinerary->transport = $transport;
        return $this;
    }
    /**
     * @return Itinerary
     */
    public function getItinerary(): Itinerary
    {
        return $this->itinerary;
    }
}
行程建造者用了流式接口 (Fluent Interface),來增加程式碼可讀性。
我們待會會在指揮者類別中展示。
(註:此處也可以實作多個不同的行程建造者,來固定某些行程選項)
需求三:實作旅行社(指揮者類別)
<?php
namespace App\BuilderPattern\Vacation;
use App\BuilderPattern\Vacation\Contracts\ItineraryPlanable;
class TravelAgency
{
    /**
     * @var ItineraryPlanable
     */
    protected $itineraryBuilder;
    public function __construct(ItineraryPlanable $itineraryBuilder)
    {
        $this->itineraryBuilder = $itineraryBuilder;
    }
    /**
     * @return array
     */
    public function getHighSpeedRailItinerary()
    {
        $itinerary = $this->itineraryBuilder
            ->from('Kaohsiung')
            ->to('Taipei')
            ->travelBy('High Speed Rail')
            ->spendDays(1)
            ->getItinerary();
        return $itinerary->toArray();
    }
    /**
     * @return array
     */
    public function getFiveDaysTokyoItinerary()
    {
        $itinerary = $this->itineraryBuilder
            ->from('Kaohsiung')
            ->to('Tokyo')
            ->travelBy('Airplane')
            ->spendDays(5)
            ->stayAt('Disney Hotel')
            ->getItinerary();
        return $itinerary->toArray();
    }
}
透過旅行社 (指揮者類別),我們封裝了行程的實作。
使得客戶端不用知道行程的建造過程。
<?php
namespace App\BuilderPattern\Vacation;
use App\BuilderPattern\Vacation\TravelAgency;
use App\BuilderPattern\Vacation\ItineraryBuilder;
class Program
{
    /**
     * @return array
     */
    public function getDomesticTravel()
    {
        //高速鐵路一日體驗
        $itineraryBuilder = new ItineraryBuilder();
        $travelAgency = new TravelAgency($itineraryBuilder);
        return $travelAgency->getHighSpeedRailItinerary();
    }
    /**
     * @return array
     */
    public function getInternationalTravel()
    {
        //東京五日遊
        $itineraryBuilder = new ItineraryBuilder();
        $travelAgency = new TravelAgency($itineraryBuilder);
        return $travelAgency->getFiveDaysTokyoItinerary();
    }
}
[單一職責原則]
我們將指揮者類別、建造者類別與產品類別,視為三種不同的職責。
由旅行社指揮行程建造者來構建行程。
[開放封閉原則]
當新增/修改行程時,我們只要調整指揮者類別。
當新增/修改行程內部的邏輯時,我們僅需修改產品類別。
[依賴反轉原則]
指揮者類別依賴於抽象的建造者介面。
建造者類別實作抽象的建造者介面。
最後附上類別圖:
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)
ʕ •ᴥ•ʔ:核心精神在於分離建造過程與產品本身的邏輯。